/*
 * Copyright (C) 2019-2022 The Kraken authors. All rights reserved.
 * Copyright (C) 2022-present The WebF authors. All rights reserved.
 */

import 'dart:io';
import 'dart:ui';

import 'package:flutter/gestures.dart';
import 'dart:math' as math;
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart' show BouncingScrollPhysics, ClampingScrollPhysics, ScrollPhysics;

import '../../css.dart';
import 'gesture_detector.dart';
import 'monodrag.dart';
import 'scroll_activity.dart';
import 'scroll_context.dart';
import 'scroll_position_with_single_context.dart';

typedef ScrollListener = void Function(double scrollOffset, AxisDirection axisDirection);

mixin _CustomTickerProviderStateMixin implements TickerProvider {
  Set<Ticker>? _tickers;

  @override
  Ticker createTicker(TickerCallback onTick) {
    _tickers ??= <_CustomTicker>{};
    final _CustomTicker result = _CustomTicker(onTick, this, debugLabel: 'created by $this');
    _tickers!.add(result);
    return result;
  }

  void _removeTicker(_CustomTicker ticker) {
    assert(_tickers != null);
    assert(_tickers!.contains(ticker));
    _tickers!.remove(ticker);
  }
}

const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
  PointerDeviceKind.touch,
  PointerDeviceKind.stylus,
  PointerDeviceKind.invertedStylus,
  PointerDeviceKind.trackpad
};

// This class should really be called _DisposingTicker or some such, but this
// class name leaks into stack traces and error messages and that name would be
// confusing. Instead we use the less precise but more anodyne "_WidgetTicker",
// which attracts less attention.
class _CustomTicker extends Ticker {
  _CustomTicker(TickerCallback onTick, this._creator, {String? debugLabel}) : super(onTick, debugLabel: debugLabel);

  final _CustomTickerProviderStateMixin _creator;

  @override
  void dispose() {
    _creator._removeTicker(this);
    super.dispose();
  }
}

// Automatically determine scroll physics due to devices.
ScrollPhysics createScrollPhysics({ScrollPhysics? parent}) {
  if (Platform.isIOS || Platform.isMacOS) {
    return BouncingScrollPhysics(parent: parent);
  } else {
    // The default scroll physics for platforms otherwise iOS/macOS.
    return ClampingScrollPhysics(parent: parent);
  }
}

class WebFScrollable with _CustomTickerProviderStateMixin implements ScrollContext {
  late AxisDirection _axisDirection;
  ScrollPositionWithSingleContext? position;
  final ScrollPhysics _physics = createScrollPhysics();
  DragStartBehavior dragStartBehavior;
  ScrollListener? scrollListener;
  final Set<PointerDeviceKind> dragDevices;
  late CSSOverflowType _overflowType;
  final FlutterView currentView;

  WebFScrollable({
    required this.currentView,
    AxisDirection axisDirection = AxisDirection.down,
    CSSOverflowType overflowType = CSSOverflowType.scroll,
    this.dragStartBehavior = DragStartBehavior.start,
    this.scrollListener,
    this.dragDevices = _kTouchLikeDeviceTypes}) {
    _axisDirection = axisDirection;
    _overflowType = overflowType;
    position = ScrollPositionWithSingleContext(physics: _physics, context: this, oldPosition: null, devicePixelRatio: currentView.devicePixelRatio);
  }

  /// The axis along which the scroll view scrolls.
  ///
  /// Determined by the [axisDirection].
  Axis get axis => axisDirectionToAxis(_axisDirection);

  void set overflowType(CSSOverflowType type) {
    _overflowType = type;
  }

  void handlePointerDown(PointerDownEvent event) {
    for (GestureRecognizer? recognizer in _recognizers.values) {
      recognizer!.addPointer(event);
    }
  }

  void handlePointerPanZoomStart(PointerPanZoomStartEvent event) {
    for (final GestureRecognizer? recognizer in _recognizers.values) {
      recognizer!.addPointerPanZoom(event);
    }
  }

  void handlePinterSignal(PointerSignalEvent event) {
    _receivedPointerSignal(event);
  }

  @override
  AxisDirection get axisDirection => _axisDirection;

  // This field is set during layout, and then reused until the next time it is set.
  Map<Type, GestureRecognizerFactory> _gestureRecognizers = const <Type, GestureRecognizerFactory>{};
  Map<Type, GestureRecognizer?> _recognizers = const <Type, GestureRecognizer?>{};
  bool? _lastCanDrag;
  Axis? _lastAxisDirection;

  @override
  void setCanDrag(bool canDrag) {
    PlatformDispatcher.instance.views;
    DeviceGestureSettings gestureSettings = DeviceGestureSettings.fromView(currentView);

    // Break no use update drag logic
    if (canDrag == _lastCanDrag &&
        axis == _lastAxisDirection &&
        (canDrag && _gestureRecognizers.keys.isNotEmpty || !canDrag && _gestureRecognizers.keys.isEmpty)) {
      return;
    }
    // Only this case can update to scroll mode,
    // else no scroll mode
    if (canDrag && _overflowType != CSSOverflowType.hidden) {
      switch (axis) {
        case Axis.vertical:
        // Vertical drag gesture recognizer to trigger vertical scroll.
          _gestureRecognizers = <Type, GestureRecognizerFactory>{
            ScrollVerticalDragGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<ScrollVerticalDragGestureRecognizer>(
                  () => ScrollVerticalDragGestureRecognizer(supportedDevices: dragDevices),
                  (ScrollVerticalDragGestureRecognizer instance) {
                instance
                  ..isAcceptedDrag = _isAcceptedVerticalDrag
                  ..onDown = _handleDragDown
                  ..onStart = _handleDragStart
                  ..onUpdate = _handleDragUpdate
                  ..onEnd = _handleDragEnd
                  ..onCancel = _handleDragCancel
                  ..minFlingDistance = _physics.minFlingDistance
                  ..minFlingVelocity = _physics.minFlingVelocity
                  ..maxFlingVelocity = _physics.maxFlingVelocity
                  ..gestureSettings = gestureSettings
                  ..dragStartBehavior = dragStartBehavior;
              },
            ),
          };
          break;
        case Axis.horizontal:
        // Horizontal drag gesture recognizer to horizontal vertical scroll.
          _gestureRecognizers = <Type, GestureRecognizerFactory>{
            ScrollHorizontalDragGestureRecognizer:
            GestureRecognizerFactoryWithHandlers<ScrollHorizontalDragGestureRecognizer>(
                  () => ScrollHorizontalDragGestureRecognizer(supportedDevices: dragDevices),
                  (ScrollHorizontalDragGestureRecognizer instance) {
                instance
                  ..isAcceptedDrag = _isAcceptedHorizontalDrag
                  ..onDown = _handleDragDown
                  ..onStart = _handleDragStart
                  ..onUpdate = _handleDragUpdate
                  ..onEnd = _handleDragEnd
                  ..onCancel = _handleDragCancel
                  ..minFlingDistance = _physics.minFlingDistance
                  ..minFlingVelocity = _physics.minFlingVelocity
                  ..maxFlingVelocity = _physics.maxFlingVelocity
                  ..gestureSettings = gestureSettings
                  ..dragStartBehavior = dragStartBehavior;
              },
            ),
          };
          break;
      }
    } else {
      _gestureRecognizers = const <Type, GestureRecognizerFactory>{};
    }

    _lastCanDrag = canDrag;
    _lastAxisDirection = axis;
    _syncAll(_gestureRecognizers);
  }

  // Used in the Arena to judge whether the vertical drag gesture can trigger the current container to scroll.
  bool _isAcceptedVerticalDrag(AxisDirection direction) {
    ScrollDragController drag = _drag!;
    double pixels = drag.pixels!;
    double maxScrollExtent = drag.maxScrollExtent!;
    double minScrollExtent = drag.minScrollExtent!;

    return !((direction == AxisDirection.down &&
        (pixels <= minScrollExtent || nearEqual(pixels, minScrollExtent, Tolerance.defaultTolerance.distance))) ||
        direction == AxisDirection.up &&
            (pixels >= maxScrollExtent || nearEqual(pixels, maxScrollExtent, Tolerance.defaultTolerance.distance)));
  }

  // Used in the Arena to judge whether the horizontal drag gesture can trigger the current container to scroll.
  bool _isAcceptedHorizontalDrag(AxisDirection direction) {
    ScrollDragController drag = _drag!;
    double pixels = drag.pixels!;
    double maxScrollExtent = drag.maxScrollExtent!;
    double minScrollExtent = drag.minScrollExtent!;
    return !((direction == AxisDirection.right &&
        (pixels <= minScrollExtent || nearEqual(pixels, minScrollExtent, Tolerance.defaultTolerance.distance))) ||
        direction == AxisDirection.left &&
            (pixels >= maxScrollExtent || nearEqual(pixels, maxScrollExtent, Tolerance.defaultTolerance.distance)));
  }

  void _syncAll(Map<Type, GestureRecognizerFactory> gestures) {
    final Map<Type, GestureRecognizer?> oldRecognizers = _recognizers;
    _recognizers = <Type, GestureRecognizer?>{};
    for (Type type in gestures.keys) {
      assert(gestures[type] != null);
      assert(!_recognizers.containsKey(type));
      _recognizers[type] = oldRecognizers[type] ?? gestures[type]!.constructor();
      assert(_recognizers[type].runtimeType == type,
      'GestureRecognizerFactory of type $type created a GestureRecognizer of type ${_recognizers[type]
          .runtimeType}. The GestureRecognizerFactory must be specialized with the type of the class that it returns from its constructor method.');
      gestures[type]!.initializer(_recognizers[type]);
    }
    for (Type type in oldRecognizers.keys) {
      if (!_recognizers.containsKey(type)) oldRecognizers[type]!.dispose();
    }
  }

  // TOUCH HANDLERS
  ScrollDragController? _drag;
  ScrollHoldController? _hold;

  void _handleDragDown(DragDownDetails details) {
    assert(_drag == null);
    assert(_hold == null);
    _hold = position!.hold(_disposeHold);
  }

  void _handleDragStart(DragStartDetails details) {
    // It's possible for _hold to become null between _handleDragDown and
    // _handleDragStart, for example if some user code calls jumpTo or otherwise
    // triggers a new activity to begin.
    assert(_drag == null);
    _drag = position!.drag(details, _disposeDrag);
    assert(_drag != null);
    assert(_hold == null);
  }

  void _handleDragUpdate(DragUpdateDetails details) {
    // _drag might be null if the drag activity ended and called _disposeDrag.
    assert(_hold == null || _drag == null);
    _drag?.update(details);
  }

  void _receivedPointerSignal(PointerSignalEvent event) {
    if (event is PointerScrollEvent && position != null) {
      if (!_physics.shouldAcceptUserOffset(position!)) {
        return;
      }
      final double delta = _pointerSignalEventDelta(event);
      final double targetScrollOffset = _targetScrollOffsetForPointerScroll(delta);
      // Only express interest in the event if it would actually result in a scroll.
      if (delta != 0.0 && targetScrollOffset != position?.pixels) {
        GestureBinding.instance.pointerSignalResolver.register(event, _handlePointerScroll);
      }
    }
  }

  double _pointerSignalEventDelta(PointerScrollEvent event) {
    double delta = axis == Axis.horizontal ? event.scrollDelta.dx : event.scrollDelta.dy;

    if (axisDirectionIsReversed(axisDirection)) {
      delta *= -1;
    }
    return delta;
  }

  double _targetScrollOffsetForPointerScroll(double delta) {
    return math.min(
      math.max(position!.pixels + delta, position!.minScrollExtent),
      position!.maxScrollExtent,
    );
  }

  void _handlePointerScroll(PointerSignalEvent event) {
    assert(event is PointerScrollEvent);
    final double delta = _pointerSignalEventDelta(event as PointerScrollEvent);
    final double targetScrollOffset = _targetScrollOffsetForPointerScroll(delta);
    if (delta != 0.0 && targetScrollOffset != position!.pixels) {
      position!.pointerScroll(delta);
    }
  }

  void _handleDragEnd(DragEndDetails details) {
    // _drag might be null if the drag activity ended and called _disposeDrag.
    assert(_hold == null || _drag == null);
    _drag?.end(details);
    assert(_drag == null);
  }

  void _handleDragCancel() {
    // _hold might be null if the drag started.
    // _drag might be null if the drag activity ended and called _disposeDrag.
    assert(_hold == null || _drag == null);
    _hold?.cancel();
    _drag?.cancel();
    assert(_hold == null);
    assert(_drag == null);
  }

  void _disposeHold() {
    _hold = null;
  }

  void _disposeDrag() {
    _drag = null;
  }

  @override
  void setSemanticsActions(Set<SemanticsAction?>? actions) {}

  @override
  TickerProvider get vsync => this;
}
